<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"><html
xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en"><head><link
rel='stylesheet' href='http://blogs.sitepoint.com/wp-content/plugins/wp-minify/min/?f=wp-content/themes/sitepoint_2010/style.css,wp-content/themes/sitepoint_2010/blogs-post.css,wp-content/themes/sitepoint_2010/blogs-tables.css,wp-content/plugins/openid/f/openid.css&amp;m=1300947577' type='text/css' media='screen' /> <script type='text/javascript' src='http://blogs.sitepoint.com/wp-content/plugins/wp-minify/min/?f=wp-includes/js/comment-reply.js,wp-includes/js/swfobject.js,wp-content/plugins/podcasting/player/audio-player-noswfobject.js,wp-content/themes/sitepoint_2010/js/cs5bubbles.js,wp-content/themes/sitepoint_2010/js/blogs.js,wp-content/themes/sitepoint_2010/js/ampersands.js&amp;m=1300947577'></script> <meta
http-equiv="Content-Type" content="text/html; charset=UTF-8" /><title>Cache it! Solve PHP Performance Problems » PHP » SitePoint Blogs</title><meta
name="description" content="In the good old days when building web sites was as easy as knocking up a few HTML pages, the" /><meta
name="keywords" content="This, CacheLite, HTTP, HTML, When, PEARCacheLite" /><meta
name="robots" content="index" /><meta
name="generator" content="WordPress 3.0.5" /><link
rel="shortcut icon" href="http://blogs.sitepoint.netdna-cdn.com/wp-content/themes/sitepoint_2010/favicon.ico" /><link
rel="apple-touch-icon" href="http://blogs.sitepoint.netdna-cdn.com/wp-content/themes/sitepoint_2010/images/apple-touch-icon.png" /><link
rel="image_src" href="" /><link
rel="stylesheet" type="text/css" href="http://blogs.sitepoint.netdna-cdn.com/wp-content/themes/sitepoint_2010/blogs-print.css" media="print" /><link
rel="alternate" type="application/rss+xml" title="RSS 2.0" href="http://blogs.sitepoint.com/feed/" /><link
rel="alternate" type="text/xml" title="RSS .92" href="http://blogs.sitepoint.com/feed/rss/" /><link
rel="alternate" type="application/atom+xml" title="Atom 0.3" href="http://blogs.sitepoint.com/feed/atom/" /><link
rel="pingback" href="http://blogs.sitepoint.com/xmlrpc.php" /><link
rel='archives' title='March 2011' href='http://blogs.sitepoint.com/2011/03/' /><link
rel='archives' title='February 2011' href='http://blogs.sitepoint.com/2011/02/' /><link
rel='archives' title='January 2011' href='http://blogs.sitepoint.com/2011/01/' /><link
rel='archives' title='December 2010' href='http://blogs.sitepoint.com/2010/12/' /><link
rel='archives' title='November 2010' href='http://blogs.sitepoint.com/2010/11/' /><link
rel='archives' title='October 2010' href='http://blogs.sitepoint.com/2010/10/' /><link
rel='archives' title='September 2010' href='http://blogs.sitepoint.com/2010/09/' /><link
rel='archives' title='August 2010' href='http://blogs.sitepoint.com/2010/08/' /><link
rel='archives' title='July 2010' href='http://blogs.sitepoint.com/2010/07/' /><link
rel='archives' title='June 2010' href='http://blogs.sitepoint.com/2010/06/' /><link
rel='archives' title='May 2010' href='http://blogs.sitepoint.com/2010/05/' /><link
rel='archives' title='April 2010' href='http://blogs.sitepoint.com/2010/04/' /><link
rel='archives' title='March 2010' href='http://blogs.sitepoint.com/2010/03/' /><link
rel='archives' title='February 2010' href='http://blogs.sitepoint.com/2010/02/' /><link
rel='archives' title='January 2010' href='http://blogs.sitepoint.com/2010/01/' /><link
rel='archives' title='December 2009' href='http://blogs.sitepoint.com/2009/12/' /><link
rel='archives' title='November 2009' href='http://blogs.sitepoint.com/2009/11/' /><link
rel='archives' title='October 2009' href='http://blogs.sitepoint.com/2009/10/' /><link
rel='archives' title='September 2009' href='http://blogs.sitepoint.com/2009/09/' /><link
rel='archives' title='August 2009' href='http://blogs.sitepoint.com/2009/08/' /><link
rel='archives' title='July 2009' href='http://blogs.sitepoint.com/2009/07/' /><link
rel='archives' title='June 2009' href='http://blogs.sitepoint.com/2009/06/' /><link
rel='archives' title='May 2009' href='http://blogs.sitepoint.com/2009/05/' /><link
rel='archives' title='April 2009' href='http://blogs.sitepoint.com/2009/04/' /><link
rel='archives' title='March 2009' href='http://blogs.sitepoint.com/2009/03/' /><link
rel='archives' title='February 2009' href='http://blogs.sitepoint.com/2009/02/' /><link
rel='archives' title='January 2009' href='http://blogs.sitepoint.com/2009/01/' /><link
rel='archives' title='December 2008' href='http://blogs.sitepoint.com/2008/12/' /><link
rel='archives' title='November 2008' href='http://blogs.sitepoint.com/2008/11/' /><link
rel='archives' title='October 2008' href='http://blogs.sitepoint.com/2008/10/' /><link
rel='archives' title='September 2008' href='http://blogs.sitepoint.com/2008/09/' /><link
rel='archives' title='August 2008' href='http://blogs.sitepoint.com/2008/08/' /><link
rel='archives' title='July 2008' href='http://blogs.sitepoint.com/2008/07/' /><link
rel='archives' title='June 2008' href='http://blogs.sitepoint.com/2008/06/' /><link
rel='archives' title='May 2008' href='http://blogs.sitepoint.com/2008/05/' /><link
rel='archives' title='April 2008' href='http://blogs.sitepoint.com/2008/04/' /><link
rel='archives' title='March 2008' href='http://blogs.sitepoint.com/2008/03/' /><link
rel='archives' title='February 2008' href='http://blogs.sitepoint.com/2008/02/' /><link
rel='archives' title='January 2008' href='http://blogs.sitepoint.com/2008/01/' /><link
rel='archives' title='December 2007' href='http://blogs.sitepoint.com/2007/12/' /><link
rel='archives' title='November 2007' href='http://blogs.sitepoint.com/2007/11/' /><link
rel='archives' title='October 2007' href='http://blogs.sitepoint.com/2007/10/' /><link
rel='archives' title='September 2007' href='http://blogs.sitepoint.com/2007/09/' /><link
rel='archives' title='August 2007' href='http://blogs.sitepoint.com/2007/08/' /><link
rel='archives' title='July 2007' href='http://blogs.sitepoint.com/2007/07/' /><link
rel='archives' title='June 2007' href='http://blogs.sitepoint.com/2007/06/' /><link
rel='archives' title='May 2007' href='http://blogs.sitepoint.com/2007/05/' /><link
rel='archives' title='April 2007' href='http://blogs.sitepoint.com/2007/04/' /><link
rel='archives' title='March 2007' href='http://blogs.sitepoint.com/2007/03/' /><link
rel='archives' title='February 2007' href='http://blogs.sitepoint.com/2007/02/' /><link
rel='archives' title='January 2007' href='http://blogs.sitepoint.com/2007/01/' /><link
rel='archives' title='December 2006' href='http://blogs.sitepoint.com/2006/12/' /><link
rel='archives' title='November 2006' href='http://blogs.sitepoint.com/2006/11/' /><link
rel='archives' title='October 2006' href='http://blogs.sitepoint.com/2006/10/' /><link
rel='archives' title='September 2006' href='http://blogs.sitepoint.com/2006/09/' /><link
rel='archives' title='August 2006' href='http://blogs.sitepoint.com/2006/08/' /><link
rel='archives' title='July 2006' href='http://blogs.sitepoint.com/2006/07/' /><link
rel='archives' title='June 2006' href='http://blogs.sitepoint.com/2006/06/' /><link
rel='archives' title='May 2006' href='http://blogs.sitepoint.com/2006/05/' /><link
rel='archives' title='April 2006' href='http://blogs.sitepoint.com/2006/04/' /><link
rel='archives' title='March 2006' href='http://blogs.sitepoint.com/2006/03/' /><link
rel='archives' title='February 2006' href='http://blogs.sitepoint.com/2006/02/' /><link
rel='archives' title='January 2006' href='http://blogs.sitepoint.com/2006/01/' /><link
rel='archives' title='December 2005' href='http://blogs.sitepoint.com/2005/12/' /><link
rel='archives' title='November 2005' href='http://blogs.sitepoint.com/2005/11/' /><link
rel='archives' title='October 2005' href='http://blogs.sitepoint.com/2005/10/' /><link
rel='archives' title='September 2005' href='http://blogs.sitepoint.com/2005/09/' /><link
rel='archives' title='August 2005' href='http://blogs.sitepoint.com/2005/08/' /><link
rel='archives' title='July 2005' href='http://blogs.sitepoint.com/2005/07/' /><link
rel='archives' title='June 2005' href='http://blogs.sitepoint.com/2005/06/' /><link
rel='archives' title='May 2005' href='http://blogs.sitepoint.com/2005/05/' /><link
rel='archives' title='April 2005' href='http://blogs.sitepoint.com/2005/04/' /><link
rel='archives' title='March 2005' href='http://blogs.sitepoint.com/2005/03/' /><link
rel='archives' title='February 2005' href='http://blogs.sitepoint.com/2005/02/' /><link
rel='archives' title='January 2005' href='http://blogs.sitepoint.com/2005/01/' /><link
rel='archives' title='December 2004' href='http://blogs.sitepoint.com/2004/12/' /><link
rel='archives' title='November 2004' href='http://blogs.sitepoint.com/2004/11/' /><link
rel='archives' title='October 2004' href='http://blogs.sitepoint.com/2004/10/' /><link
rel='archives' title='September 2004' href='http://blogs.sitepoint.com/2004/09/' /><link
rel='archives' title='August 2004' href='http://blogs.sitepoint.com/2004/08/' /><link
rel='archives' title='July 2004' href='http://blogs.sitepoint.com/2004/07/' /><link
rel='archives' title='June 2004' href='http://blogs.sitepoint.com/2004/06/' /><link
rel='archives' title='May 2004' href='http://blogs.sitepoint.com/2004/05/' /><link
rel='archives' title='April 2004' href='http://blogs.sitepoint.com/2004/04/' /><link
rel='archives' title='March 2004' href='http://blogs.sitepoint.com/2004/03/' /><link
rel='archives' title='February 2004' href='http://blogs.sitepoint.com/2004/02/' /><link
rel='archives' title='November 2003' href='http://blogs.sitepoint.com/2003/11/' /><link
rel='archives' title='October 2003' href='http://blogs.sitepoint.com/2003/10/' /><link
rel='archives' title='September 2003' href='http://blogs.sitepoint.com/2003/09/' /><link
rel='archives' title='July 2003' href='http://blogs.sitepoint.com/2003/07/' /><link
rel='archives' title='June 2003' href='http://blogs.sitepoint.com/2003/06/' /><link
rel='archives' title='April 2003' href='http://blogs.sitepoint.com/2003/04/' /><link
rel='archives' title='March 2003' href='http://blogs.sitepoint.com/2003/03/' /><link
rel='archives' title='February 2003' href='http://blogs.sitepoint.com/2003/02/' /><link
rel='archives' title='December 2002' href='http://blogs.sitepoint.com/2002/12/' /><link
rel='archives' title='September 2002' href='http://blogs.sitepoint.com/2002/09/' /><link
rel='archives' title='April 2002' href='http://blogs.sitepoint.com/2002/04/' /><link
rel='archives' title='February 2002' href='http://blogs.sitepoint.com/2002/02/' /><link
rel='archives' title='January 2002' href='http://blogs.sitepoint.com/2002/01/' /><link
rel='archives' title='December 2001' href='http://blogs.sitepoint.com/2001/12/' /><link
rel='archives' title='September 2001' href='http://blogs.sitepoint.com/2001/09/' /><link
rel='archives' title='August 2001' href='http://blogs.sitepoint.com/2001/08/' /><link
rel='archives' title='July 2001' href='http://blogs.sitepoint.com/2001/07/' /><link
rel='archives' title='January 2001' href='http://blogs.sitepoint.com/2001/01/' /><link
rel='archives' title='November 2000' href='http://blogs.sitepoint.com/2000/11/' /><link
rel="EditURI" type="application/rsd+xml" title="RSD" href="http://blogs.sitepoint.com/xmlrpc.php?rsd" /><link
rel="wlwmanifest" type="application/wlwmanifest+xml" href="http://blogs.sitepoint.com/wp-includes/wlwmanifest.xml" /><link
rel='index' title='SitePoint Blogs' href='http://blogs.sitepoint.com/' /><link
rel='start' title='Welcome to PHPinfo!' href='http://blogs.sitepoint.com/welcome-to-phpinfo/' /><link
rel='prev' title='Friendly URLs' href='http://blogs.sitepoint.com/friendly-urls/' /><link
rel='next' title='Preparing for Rails 2.0: Controller-based exception handling' href='http://blogs.sitepoint.com/preparing-for-rails-20-controller-based-exception-handling/' /><meta
name="generator" content="WordPress 3.0.5" /><link
rel='canonical' href='http://blogs.sitepoint.com/caching-php-performance/' /><link
rel='shortlink' href='http://blogs.sitepoint.com/?p=34710' /><link
rel="stylesheet" type="text/css" href="http://blogs.sitepoint.netdna-cdn.com/wp-content/plugins/code-highlight/css/correction.css" /> <script type="text/javascript" src="http://blogs.sitepoint.netdna-cdn.com/wp-content/plugins/code-highlight/sh/Scripts/shCore.js"></script> <script type="text/javascript" src="http://blogs.sitepoint.netdna-cdn.com/wp-content/plugins/code-highlight/js/shUnobtrusive.js"></script> <script type="text/javascript" src="http://blogs.sitepoint.netdna-cdn.com/wp-content/plugins/code-highlight/js/shBrushBasic.js"></script>  <script type="text/javascript">AudioPlayer.setup("http://blogs.sitepoint.com/wp-content/plugins/podcasting/player/player.swf", {
				width: 290, bg: '0xF8F8F8', leftbg: '0x012E5C', rightbg: '0xff6600', rightbghover: '0xffff00', lefticon: '0xFFFFFF', righticon: '0xFFFFFF', righticonhover: '0x666666', text: '0x666666', slider: '0x666666', track: '0xFFFFFF', loader: '0xB5DAFF', border: '0x666666'			});</script> <script type="text/javascript" src="http://blogs.sitepoint.netdna-cdn.com/blogs/wp-content/plugins/sitepoint-ref-links/js/sitepoint-ref-links.js"></script> <link
rel="stylesheet" type="text/css" media="screen" href="http://blogs.sitepoint.netdna-cdn.com/blogs/wp-content/plugins/sitepoint-ref-links/css/sitepoint-ref-links.css" /><link
rel="alternate" type="application/rss+xml" title="Podcast: SitePoint Podcast" href="http://blogs.sitepoint.com/feed/podcast/" /> <script src="http://sitepointstatic.com/js/utilities.js" type="text/javascript"></script> <script type="text/javascript" src="http://partner.googleadservices.com/gampad/google_service.js"></script><script type="text/javascript">GS_googleAddAdSenseService("ca-pub-9603645151104616"); GS_googleEnableAllServices();</script><script type="text/javascript">GA_googleAddSlot("ca-pub-9603645151104616", "Blogs_10b_728x90_Exclusive_Sponsorship"); GA_googleAddSlot("ca-pub-9603645151104616", "Blogs_13a_300x100"); GA_googleAddSlot("ca-pub-9603645151104616", "Blogs_13b_300x100"); GA_googleAddSlot("ca-pub-9603645151104616", "Blogs_160x600_SkyScraper"); GA_googleAddSlot("ca-pub-9603645151104616", "Blogs_10a_300x250_Exclusive_Sponsorship"); GA_googleAddSlot("ca-pub-9603645151104616", "Blogs_300x250_2"); GA_googleAddSlot("ca-pub-9603645151104616", "Home_Page_1_250x250"); GA_googleAddSlot("ca-pub-9603645151104616", "Home_Page_2_250x250"); GA_googleAddSlot("ca-pub-9603645151104616", "Home_Page_3_250x250"); GA_googleAddSlot("ca-pub-9603645151104616", "Home_Page_4_250x250"); GA_googleAddSlot("ca-pub-9603645151104616", "Home_Page_728x90"); GA_googleAddSlot("ca-pub-9603645151104616", "Home_Page_160x600_Skyscraper"); GA_googleAddSlot("ca-pub-9603645151104616", "Home_Page_300x100_A"); GA_googleAddSlot("ca-pub-9603645151104616", "Home_Page_300x100_B"); GA_googleAddSlot("ca-pub-9603645151104616", "Blogs_250x250_1"); GA_googleAddSlot("ca-pub-9603645151104616", "Blogs_250x250_2"); GA_googleAddSlot("ca-pub-9603645151104616", "Blogs_250x250_3"); GA_googleAddSlot("ca-pub-9603645151104616", "Blogs_250x250_4");</script><script type="text/javascript">GA_googleFetchAds();</script> <link
rel="stylesheet" type="text/css" href="http://i2.sitepoint.com/css2/promobar.css" media="screen,projection,handheld,print" /></head><body> <script type="text/javascript">var SPMetrics_options = {
	'original_referrer': document.referrer,
	'context': 'blogs',
	'category': 'design',
	'group': 6
};</script> <script src="http://metrics.aws.sitepoint.com/bouncer" type="text/javascript"></script> <script type="text/javascript" src="http://sitepointstatic.com/frontpage/scripts/promobar.js"></script> <div
id="header"><div
id="header-content"><h2> <a
id="sitepoint-logo" href="http://www.sitepoint.com/"> <img
width="202" height="61" alt="SitePoint" src="http://1.sitepointstatic.com/frontpage/images/logo.png" /></a></h2><p><q>Become a <strong>better</strong> web developer.<span></span></q></p></div><h2 class="structural-label">Site navigation</h2><div
id="navigation"><ul><li ><a
title="The best place to begin your SitePoint session" href="http://www.sitepoint.com/">Home</a></li><li><a
title="SitePoint&#8217;s renowned web development training products" href="http://products.sitepoint.com/">Products</a></li><li><a
title="SitePoint&#8217;s renowned web development training courses" href="http://courses.sitepoint.com/">Courses</a></li><li><a
title="Our friendly, vibrant and well-informed community" href="http://www.sitepoint.com/forums/">Forums</a></li><li ><a
title="Daily comment across a range of web subjects by some of the top experts in their field" href="http://blogs.sitepoint.com/">Blogs</a></li><li ><a
title="Weekly podcast with news and commentary for professional web developers and designers" href="http://www.sitepoint.com/podcast/">Podcast</a></li><li><a
title="One of the web&#8217;s largest repositories of &#8217;web know-how&#8217;" href="http://www.sitepoint.com/recentarticles/">Articles</a></li><li><a
title="The Web&#8217;s most complete and up-to-date reference" href="http://reference.sitepoint.com/">Reference</a></li><li
class="newribbon"><a
title="Buy and sell goods and services in one of the world&#8217;s largest web communities" target="_blank" href="http://marketplace.sitepoint.com/">Marketplace</a></li><li><a
title="The place to go for help with anything to do with SitePoint" href="http://www.sitepoint.com/help/">Help</a></li></ul><h2 class="structural-label">Search this site</h2><form
action="http://search.sitepoint.com/" method="get" id="search"><fieldset
class="query"> <input
type="hidden" value="10" name="ps" /> <input
type="text" value="Search this site" id="mainsearchbox" name="q" class="text defaultValue" /> <input
type="image" class="image" src="http://sitepointstatic.com/frontpage/images/search-glassgobutton.png" alt="GO" /></fieldset></form></div></div><div
id="page-header" class="page"> <span
id="header-shadow"></span><div
id="content-header" class="content"><div
id="adTopBanner1"> <script type="text/javascript">GA_googleFillSlot("Blogs_10b_728x90_Exclusive_Sponsorship");</script> </div><div
id="rss-links"><h2>Subscribe via RSS</h2><p> <a
href="http://www.sitepoint.com/feed/" title="All Posts"> <img
src="http://blogs.sitepoint.netdna-cdn.com/wp-content/themes/sitepoint_2010/images/rss90x100.png" alt="RSS" /></a></p><ul><li><a
href="/feed">All Posts</a></li><li><a
href="http://blogs.sitepoint.com/category/tech/feed/">Tech Feed</a></li><li><a
href="http://blogs.sitepoint.com/category/design/feed/">Design Feed</a></li><li><a
href="http://blogs.sitepoint.com/category/business/feed/">Business Feed</a></li></ul></div><h2 class="structural-label">Blog categories</h2><ol
id="categories"><li> <a
href="/" title="All (current category)">All</a></li><li> <a
href="http://blogs.sitepoint.com/category/tech">Tech</a></li><li> <a
href="http://blogs.sitepoint.com/category/design">Design</a></li><li> <a
href="http://blogs.sitepoint.com/category/business">Business</a></li><li> <a
href="http://blogs.sitepoint.com/category/community">Community</a></li><li> <a
href="http://blogs.sitepoint.com/category/podcast">Podcast</a></li></ol><div
class="clear"></div></div></div><div
id="page" class="page"><div
id="content" class="content"><div
id="primary-content" class="column"><div
id="post-intro" class="intro"><h1>Cache it! Solve PHP Performance Problems</h1><dl
class="details"><dt
class="author structural-label">Author</dt><dd
class="author"> <a
href="http://blogs.sitepoint.com/author/ben-dechrau/" title="Posts by Ben Dechrau">Ben Dechrau</a></dd><dt
class="department structural-label">Department</dt><dd
class="department"> <a
href="/category/php" title="More in &#8220;PHP&#8221;">PHP</a></dd><dt
class="comments structural-label">Comments</dt><dd
class="comments"> <span>Comments Off</span></dd></dl><dl
id="social-badges"><dt
class="structural-label">Diggs</dt><dd
class="diggthis" id="socialbadge1"></dd><dt
class="structural-label">Tweets</dt><dd
class="retweet"> <a
href="http://twitter.com/share"
class="twitter-share-button"
data-url="http://blogs.sitepoint.com/caching-php-performance/"
data-count="vertical"
data-via="sitepointdotcom"
>Tweet</a> <script type="text/javascript" src="http://platform.twitter.com/widgets.js"></script> </dd><dt
class="structural-label">Delicious</dt><dd
class="delicious" id="socialbadge3"></dd></dl></div><div
id="post-content"><div
id="adz" class="ad size300x250"><script type="text/javascript">GA_googleFillSlot("Blogs_10a_300x250_Exclusive_Sponsorship");</script></div><p><strong>In the good old days when building web sites was as easy as knocking up a few HTML pages, the delivery of a web page to a browser was a simple matter of having the web server fetch a file. A site&#8217;s visitors would see its small, text-only pages almost immediately, unless they were using particularly slow modems. Once the page was downloaded, the browser would cache it somewhere on the local computer so that, should the page be requested again, after performing a quick check with the server to ensure the page hadn&#8217;t been updated, the browser could display the locally cached version. Pages were served as quickly and efficiently as possible, and everyone was happy. </strong></p><p>Then dynamic web pages came along and spoiled the party by introducing two problems:</p><ul><li>When a request for a dynamic web page is received by the server, some intermediate processing must be completed, such as the execution of scripts by the PHP engine. This processing introduces a delay before the web server begins to deliver the output to the browser. This may not be a significant delay where simple PHP scripts are concerned, but for a more complex application, the PHP engine may have a lot of work to do before the page is finally ready for delivery. This extra work results in a noticeable time lag between the user&#8217;s requests and the actual display of pages in the browser.</li><li>A typical web server, such as Apache, uses the time of file modification to inform a web browser of a requested page&#8217;s age, allowing the browser to take appropriate caching action. With dynamic web pages, the actual PHP script may change only occasionally; meanwhile, the content it displays, which is often fetched from a database, will change frequently. The web server has no way of discerning updates to the database, so it doesn&#8217;t send a last modified date. If the client (that is, the user&#8217;s browser) has no indication of how long the data will remain valid, it will take a guess. This is problematic if the browser decides to use a locally cached version of the page which is now out of date, or if the browser decides to request from the server a fresh copy of the page, which actually has no new content, making the request redundant. The web server will always respond with a freshly constructed version of the page, regardless of whether or not the data in the database has actually changed.</li></ul><p>To avoid the possibility of a web site visitor viewing out-of-date content, most web developers use a meta tag or HTTP headers to tell the browser never to use a cached version of the page. However, this negates the web browser&#8217;s natural ability to cache web pages, and entails some serious disadvantages. For example, the content delivered by a dynamic page may only change once a day, so there&#8217;s certainly a benefit to be gained by having the browser cache a page&#8211;even if only for 24 hours.</p><p>If you&#8217;re working with a small PHP application, it&#8217;s usually possible to live with both issues. But as your site increases in complexity&#8211;and attracts more traffic&#8211;you&#8217;ll begin to run into performance problems. Both these issues can be solved, however: the first with server-side caching; the second, by taking control of client-side caching from within your application. The exact approach you use to solve these problems will depend on your application, but in this chapter, we&#8217;ll consider both PHP and a number of class libraries from PEAR as possible panaceas for your web page woes.</p><p>Note that in this chapter&#8217;s discussions of caching, we&#8217;ll look at only those solutions that can be implemented in PHP. For a more general introduction, the definitive discussion of web caching is represented by <a
class="sublink" href="http://www.mnot.net/cache_docs/">Mark Nottingham&#8217;s tutorial</a>.</p><div
id="adz" class="vertical"><script type="text/javascript">/*<![CDATA[*/// < ![CDATA[
GA_googleFillSlot("Articles_6_300x250");
// ]]&gt;/*]]>*/</script></div><p>Furthermore, the solutions in this chapter should not be confused with some of the script caching solutions that work on the basis of optimizing and caching compiled PHP scripts, such as <a
class="sublink" href="http://www.zend.com/">Zend Accelerator</a> and <a
class="sublink" href="http://www.php-accelerator.co.uk/">ionCube PHP Accelerator</a>.</p><p>This chapter is excerpted from <a
class="sublink" href="http://www.sitepoint.com/books/phpant2/"><em>The PHP Anthology: 101 Essential Tips, Tricks &amp; Hacks, 2nd Edition</em></a>. <a
class="sublink" href="www.sitepoint.com/launch/108ef2/2/120">Download this chapter plus two others, covering PDO and Databases, and Access Control</a>, in PDF format to read offline.</p><h5>How do I prevent web browsers from caching a page?</h5><p>If timely information is crucial to your web site and you wish to prevent out-of-date content from ever being visible, you need to understand how to prevent web browsers&#8211;and proxy servers&#8211;from caching pages in the first place.</p><p><strong><em>Solutions</em></strong></p><p>There are two possible approaches we could take to solving this problem: using HTML meta tags, and using HTTP headers.</p><p><strong>Using HTML Meta Tags </strong></p><p>The most basic approach to the prevention of page caching is one that utilizes HTML meta tags:</p> <code>&lt;meta http-equiv="expires" content="Mon, 26 Jul 1997 05:00:00 GMT"/&gt;
&lt;meta http-equiv="pragma" content="no-cache" /&gt;</code><p>The insertion of a date that&#8217;s already passed into the <code>Expires</code> meta tag tells the browser that the cached copy of the page is always out of date. Upon encountering this tag, the browser usually won&#8217;t cache the page. Although the <code>Pragma: no-cache</code> meta tag isn&#8217;t guaranteed, it&#8217;s a fairly well-supported convention that most web browsers follow. However, the two issues associated with this approach, which we&#8217;ll discuss below, may prompt you to look at the alternative solution.</p><p><strong>Using HTTP Headers</strong></p><p>A better approach is to use the HTTP protocol itself, with the help of PHP&#8217;s header function, to produce the equivalent of the two HTML meta tags above:</p> <code>&lt;?php
header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
header('Pragma: no-cache');
?&gt;</code><p>We can go one step further than this, using the <code>Cache-Control</code> header that&#8217;s supported by HTTP 1.1-capable browsers:</p> <code>&lt;?php
header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Cache-Control: post-check=0, pre-check=0', FALSE);
header('Pragma: no-cache');
?&gt;</code><p>For a precise description of HTTP 1.1 Cache-Control headers, have a look at <a
class="sublink" href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9">the W3C&#8217;s HTTP 1.1 RFC</a>. Another great source of information about HTTP headers, which can be applied readily to PHP, is <a
class="sublink" href="http://perl.apache.org/docs/general/correct_headers/correct_headers.html">mod_perl&#8217;s documentation on issuing correct headers</a>.</p><p><strong><em>Discussion</em></strong></p><p>Using the <code>Expires</code> meta tag sounds like a good approach, but two problems are associated with it:</p><ul><li>The browser first has to download the page in order to read the meta tags. If a tag wasn&#8217;t present when the page was first requested by a browser, the browser will remain blissfully ignorant and keep its cached copy of the original.</li><li>Proxy servers that cache web pages, such as those common to ISPs, generally won&#8217;t read the HTML documents themselves. A web browser might know that it shouldn&#8217;t cache the page, but the proxy server between the browser and the web server probably doesn&#8217;t&#8211;it will continue to deliver the same out-of-date page to the client.</li></ul><p>On the other hand, using the HTTP protocol to prevent page caching essentially guarantees that no web browser or intervening proxy server will cache the page, so visitors will always receive the latest content. In fact, the first header should accomplish this on its own; this is the best way to ensure a page is not cached. The <code>Cache-Control</code> and <code>Pragma</code> headers are added for some degree of insurance. Although they don&#8217;t work on all browsers or proxies, the <code>Cache-Control</code> and <code>Pragma</code> headers will catch some cases in which the Expires header doesn&#8217;t work as intended&#8211;if the client computer&#8217;s date is set incorrectly, for example.</p><p>Of course, to disallow caching entirely introduces the problems we discussed at the start of this chapter: it negates the web browser&#8217;s natural ability to cache pages, and can create unnecessary overhead, as new versions of pages are always requested, even though those pages may not have been updated since the browser&#8217;s last request. We&#8217;ll look at the solution to these issues in just a moment.</p><h5>How do I control client-side caching?</h5><p>We addressed the task of disabling client-side caching in &#8220;How do I prevent web browsers from caching a page?&#8221;, but disabling the cache is rarely the only (or best) option.</p><p>Here we&#8217;ll look at a mechanism that allows us to take advantage of client-side caches in a way that can be controlled from within a PHP script.</p><p><em>Apache Required!</em><br
/> <em>This approach will only work if you&#8217;re running PHP as an Apache web server module, because it requires use of the function getallheaders&#8211;which only works with Apache&#8211;to fetch the HTTP headers sent by a web browser.</em></p><p><strong><em>Solutions</em></strong></p><p>In controlling client-side caching you have two alternatives. You can set a date on which the page will expire, or respond to the browser&#8217;s request headers. Let&#8217;s see how each of these tactics is executed.</p><p><strong>Setting a Page Expiry Header</strong></p><p>The header that&#8217;s easiest to implement is the <code>Expires</code> header&#8211;we use it to set a date on which the page will expire, and until that time, web browsers are allowed to use a cached version of the page. Here&#8217;s an example of this header at work:</p> <code>expires.php (excerpt) </code><p>&lt;?php<br
/> function setExpires($expires) {<br
/> header(<br
/> &#8216;Expires: &#8216;.gmdate(&#8216;D, d M Y H:i:s&#8217;, time()+$expires).&#8217;GMT&#8217;);<br
/> }<br
/> setExpires(10);<br
/> echo ( &#8216;This page will self destruct in 10 seconds&lt;br /&gt;&#8217; );<br
/> echo ( &#8216;The GMT is now &#8216;.gmdate(&#8216;H:i:s&#8217;).&#8217;&lt;br /&gt;&#8217; );<br
/> echo ( &#8216;&lt;a href=&#8221;&#8216;.$_SERVER['PHP_SELF'].&#8217;&#8221;&gt;View Again&lt;/a&gt;&lt;br /&gt;&#8217; );<br
/> ?&gt;</p><p>In this example, we created a custom function called <code>setExpires</code> that sets the HTTP <code>Expires</code> header to a point in the future, defined in seconds. The output of the above example shows the current time in GMT, and provides a link that allows us to view the page again. If we follow this link, we&#8217;ll notice the time updates only once every ten seconds. If you like, you can also experiment by using your browser&#8217;s Refresh button to tell the browser to refresh the cache, and watching what happens to the displayed date.</p><p><strong>Acting on the Browser&#8217;s Request Headers</strong></p><p>A more useful approach to client-side cache control is to make use of the <code>Last-Modified</code> and <code>If-Modified-Since</code> headers, both of which are available in HTTP 1.0. This action is known technically as performing a conditional GET request; whether your script returns any content depends on the value of the incoming <code>If-Modified-Since</code> request header.</p><p>If you use PHP version 4.3.0 and above on Apache, the HTTP headers are accessible with the functions <code>apache_request_headers</code> and <code>apache_response_headers</code>. Note that the function <code>getallheaders</code> has become an alias for the new <code>apache_request_headers</code> function.</p><div
id="adz" class="vertical"><script type="text/javascript">/*<![CDATA[*/// < ![CDATA[
GA_googleFillSlot("Articles_6_300x250");
// ]]&gt;/*]]>*/</script></div><p>This approach requires that you send a <code>Last-Modified</code> header every time your PHP script is accessed. The next time the browser requests the page, it sends an <code>If-Modified-Since</code> header containing a time; your script can then identify whether the page has been updated since that time. If it hasn&#8217;t, your script sends an HTTP 304 status code to indicate that the page hasn&#8217;t been modified, and exits before sending the body of the page.</p><p>Let&#8217;s see these headers in action. The example below uses the modification date of a text file. To simulate updates, we first need to create a way to randomly write to the file:</p> <code>ifmodified.php (excerpt)
&lt;?php
$file = 'ifmodified.txt';
$random = array (0,1,1);
shuffle($random);
if ( $random[0] == 0 ) {
$fp = fopen($file, 'w');
fwrite($fp, 'x');
fclose($fp);
}
$lastModified = filemtime($file);</code><p>Our simple randomizer provides a one-in-three chance that the file will be updated each time the page is requested. We also use the <code>filemtime</code> function to obtain the last modified time of the file.</p><p>Next, we send a <code>Last-Modified</code> header that uses the modification time of the text file. We need to send this header for every page we render, to cause visiting browsers to send us the <code>If-Modifed-Since</code> header upon every request:</p> <code>ifmodified.php (excerpt)
header('Last-Modified: ' .
gmdate('D, d M Y H:i:s', $lastModified) . ' GMT');</code><p>Our use of the <code>getallheaders</code> function ensures that PHP gives us all the incoming request headers as an array. We then need to check that the If-Modified-Since header actually exists; if it does, we have to deal with a special case caused by older Mozilla browsers (earlier than version 6), which appended an illegal extra field to their <code>If-Modified-Since</code> headers. We use PHP&#8217;s <code>strtotime</code> function to generate a timestamp from the date the browser sent us. If there&#8217;s no such header, we set this timestamp to zero, which forces PHP to give the visitor an up-to-date copy of the page:</p> <code>ifmodified.php (excerpt)
$request = getallheaders();
if (isset($request['If-Modified-Since']))
{
$modifiedSince = explode(';', $request['If-Modified-Since']);
$modifiedSince = strtotime($modifiedSince[0]);
}
else
{
$modifiedSince = 0;
}</code><p>Finally, we check to see whether or not the cache has been modified since the last time the visitor received this page. If it hasn&#8217;t, we simply send a <code>304 Not Modified</code> response header and exit the script, saving bandwidth and processing time by prompting the browser to display its cached copy of the page:</p> <code>ifmodified.php (excerpt)
if ($lastModified &lt;= $modifiedSince)
{
header('HTTP/1.1 304 Not Modified');
exit();
}
echo ( 'The GMT is now '.gmdate('H:i:s').'&lt;br /&gt;' );
echo ( '&lt;a href="'.$_SERVER['PHP_SELF'].'"&gt;View Again&lt;/a&gt;&lt;br /&gt;' );
?&gt;</code><p>Remember to use the &#8220;View Again&#8221; link when you run this example (clicking the Refresh button usually clears your browser&#8217;s cache). If you click on the link repeatedly, the cache will eventually be updated; your browser will throw out its cached version and fetch a new page from the server.</p><p>If you combine the <code>Last-Modified</code> header approach with time values that are already available in your application&#8211;for example, the time of the most recent news article&#8211;you should be able to take advantage of web browser caches, saving bandwidth and improving your application&#8217;s perceived performance in the process.</p><p>Be very careful to test any caching performed in this manner, though; if you get it wrong, you may cause your visitors to consistently see out-of-date copies of your site.</p><p><strong><em>Discussion</em></strong></p><p>HTTP dates are always calculated relative to Greenwich Mean Time (GMT). The PHP function gmdate is exactly the same as the date function, except that it automatically offsets the time to GMT based on your server&#8217;s system clock and regional settings.</p><p>When a browser encounters an <code>Expires</code> header, it caches the page. All further requests for the page that are made before the specified expiry time use the cached version of the page&#8211;no request is sent to the web server. Of course, client-side caching is only truly effective if the system time on the computer is accurate. If the computer&#8217;s time is out of sync with that of the web server, you run the risk of pages either being cached improperly, or never being updated.</p><p>The <code>Expires</code> header has the advantage that it&#8217;s easy to implement; in most cases, however, unless you&#8217;re a highly organized person, you won&#8217;t know exactly when a given page on your site will be updated. Since the browser will only contact the server after the page has expired, there&#8217;s no way to tell browsers that the page they&#8217;ve cached is out of date. In addition, you also lose some knowledge of the traffic visiting your web site, since the browser will not make contact with the server when it requests a page that&#8217;s been cached.</p><h5>How do I examine HTTP headers in my browser?</h5><p>How can you actually check that your application is running as expected, or debug your code, if you can&#8217;t actually see the HTTP headers? It&#8217;s worth knowing exactly which headers your script is sending, particularly when you&#8217;re dealing with HTTP cache headers.</p><p><strong><em>Solution</em></strong><em> </em></p><p>Several worthy tools are available to help you get a closer look at your HTTP headers:</p><p><a
class="sublink" href="http://livehttpheaders.mozdev.org/"><strong>LiveHTTPHeaders</strong></a><br
/> This add-on to the Firefox browser is a simple but very handy tool for examining request and response headers while you&#8217;re browsing.</p><p><a
class="sublink" href="http://getfirebug.org/"><strong>Firebug</strong></a><br
/> Another useful Firefox add-on, Firebug is a tool whose interface offers a dedicated tab for examining HTTP request information.</p><p><a
class="sublink" href="http://www.httpwatch.com/"><strong>HTTPWatch</strong></a><br
/> This add-on to Internet Explorer for HTTP viewing and debugging is similar to LiveHTTPHeaders above.</p><p><a
class="sublink" href="http://getcharles.com/"><strong>Charles Web Debugging Proxy</strong></a><br
/> Available for Windows, Mac OS X, and Linux or Unix, the Charles Web Debugging Proxy is a proxy server that allows developers to see all the HTTP traffic between their browsers and the web servers to which they connect.</p><p>Any of these tools will allow you to inspect the communication between the server and browser.</p><h5>How do I cache file downloads with Internet Explorer?</h5><p>If you&#8217;re developing file download scripts for Internet Explorer users, you might notice a few issues with the download process. In particular, when you&#8217;re serving a file download through a PHP script that uses headers such as <code>Content-Disposition: attachment, filename=myFile.pdf</code> or <code>Content-Disposition: inline, filename=myFile.pdf</code>, and that tells the browser not to cache pages, Internet Explorer won&#8217;t deliver that file to the user.</p><p><strong><em>Solutions</em></strong></p><p>Internet Explorer handles downloads in a rather unusual manner: it makes two requests to the web site. The first request downloads the file and stores it in the cache before making a second request, the response to which is not stored. The second request invokes the process of delivering the file to the end user in accordance with the file&#8217;s type&#8211;for instance, it starts Acrobat Reader if the file is a PDF document. Therefore, if you send the cache headers that instruct the browser not to cache the page, Internet Explorer will delete the file between the first and second requests, with the unfortunate result that the end user receives nothing!</p><p>If the file you&#8217;re serving through the PHP script won&#8217;t change, one solution to this problem is simply to disable the &#8220;don&#8217;t cache&#8221; headers, <code>pragma</code> and <code>cache-control</code>, which we discussed in &#8220;How do I prevent web browsers from caching a page?&#8221;, for the download script.</p><p>If the file download will change regularly, and you want the browser to download an up-to-date version of it, you&#8217;ll need to use the <code>Last-Modified</code> header that we met in &#8220;How do I control client-side caching?&#8221;, and ensure that the time of modification remains the same across the two consecutive requests. You should be able to achieve this goal without affecting users of browsers that handle downloads correctly.</p><p>One final solution is to write the file to the file system of your web server and simply provide a link to it, leaving it to the web server to report the cache headers for you. Of course, this may not be a viable option if the file is supposed to be secured.</p><h5>How do I use output buffering for server-side caching?</h5><p>Server-side processing delay is one of the biggest bugbears of dynamic web pages. We can reduce server-side delay by caching output. The page is generated normally, performing database queries and so on with PHP; however, before sending it to the browser, we capture and store the finished page somewhere&#8211;in a file, for instance. The next time the page is requested, the PHP script first checks to see whether a cached version of the page exists. If it does, the script sends the cached version straight to the browser, avoiding the delay involved in rebuilding the page.</p><p><strong><em>Solution</em></strong></p><p>Here, we&#8217;ll look at PHP&#8217;s in-built caching mechanism, the output buffer, which can be used with whatever page rendering system you prefer (templates or no templates). Consider situations in which your script displays results using, for example, echo or print, rather than sending the data directly to the browser. In such cases, you can use PHP&#8217;s output control functions to store the data in an in-memory buffer, which your PHP script has both access to and control over.</p><p>Here&#8217;s a simple example that demonstrates how the output buffer works:</p> <code>buffer.php (excerpt)
&lt;?php
ob_start();
echo '1. Place this in the buffer&lt;br /&gt;';
$buffer = ob_get_contents();
ob_end_clean();
echo '2. A normal echo&lt;br /&gt;';
echo $buffer;
?&gt;</code><p>The buffer itself stores the output as a string. So, in the above script, we commence buffering with the <code>ob_startfunction</code>, and use <code>echo</code> to display a piece of text which is stored in the output buffer automatically. We then use the <code>ob_get_contents</code> function to fetch the data the echo statement placed in the buffer, and store it in the <code>$buffer</code> variable. The <code>ob_end_clean</code> function stops the output buffer and empties the contents; the alternative approach is to use the <code>ob_end_flushfunction</code>, which displays the contents of the buffer.</p><p>The above script displays the following output:</p> <code>2. A normal echo
1. Place this in the buffer</code><p>In other words, we captured the output of the first echo, then sent it to the browser after the second echo. As this simple example suggests, output buffering can be a very powerful tool when it comes to building your site; it provides a solution for caching, as we&#8217;ll see in a moment, and is also an excellent way to hide errors from your site&#8217;s visitors, as is discussed in Chapter 9. Output buffering even provides a possible alternative to browser redirection in situations such as user authentication.</p><p>In order to improve the performance of our site, we can store the output buffer contents in a file. We can then call on this file for the next request, rather than having to rebuild the output from scratch again. Let&#8217;s look at a quick example of this technique. First, our example script checks for the presence of a cache file:</p> <code>sscache.php (excerpt)
&lt;?php
if (file_exists('./cache/page.cache'))
{
readfile('./cache/page.cache');
exit();
}</code><p>If the script finds the cache file, we simply output its contents and we&#8217;re done! If the cache file is not found, we proceed to output the page using the output buffer:</p> <code>sscache.php (excerpt)
ob_start();
?&gt;
&lt;!DOCTYPE html public "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"&gt;
&lt;html xmlns="http://www.w3.org/1999/xhtml"&gt;
&lt;head&gt;
&lt;title&gt;Cached Page&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
This page was cached with PHP's
&lt;a href="http://www.php.net/outcontrol"
&gt;Output Control Functions&lt;/a&gt;
&lt;/body&gt;
&lt;/html&gt;
&lt;?php
$buffer = ob_get_contents();
ob_end_flush();</code><p>Before we flush the output buffer to display our page, we make sure to store the buffer contents in the <code>$buffer</code> variable.</p><p>The final step is to store the saved buffer contents in a text file:</p> <code>sscache.php (excerpt)
$fp = fopen('./cache/page.cache','w');
fwrite($fp,$buffer);
fclose($fp);
?&gt;</code><p>The <code>page.cache</code> file contents are exactly same as the HTML that was rendered by the script:</p> <code>cache/page.cache (excerpt)
&lt;!DOCTYPE html public "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"&gt;
&lt;html xmlns="http://www.w3.org/1999/xhtml"&gt;
&lt;head&gt;
&lt;title&gt;Cached Page&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
This page was cached with PHP's
&lt;a href="http://www.php.net/outcontrol"
&gt;Output Control Functions&lt;/a&gt;
&lt;/body&gt;
&lt;/html&gt;</code><p><strong><em>Discussion</em></strong></p><p>For an example that shows how to use PHP&#8217;s output buffering capabilities to handle errors more elegantly, have a look at the PHP Freaks article <a
class="sublink" href="http://www.phpfreaks.com/tutorials/59/0.php"><em>Introduction to Output Buffering</em>, by Derek Ford</a>.</p><p><strong>What About Template Caching?</strong></p><div
id="adz" class="vertical"><script type="text/javascript">/*<![CDATA[*/// < ![CDATA[
GA_googleFillSlot("Articles_6_300x250");
// ]]&gt;/*]]>*/</script></div><p>Template engines often include template caching features&#8211;<a
class="sublink" href="http://smarty.php.net/">Smarty</a> is a case in point. Usually, these engines offer a built-in mechanism for storing a compiled version of a template (that is, the native PHP generated from the template), which prevents us developers from having to recompile the template every time a page is requested.</p><p>This process should not be confused with output&#8211;or content&#8211;caching, which refers to the caching of the rendered HTML (or other output) that PHP sends to the browser. In addition to the content cache mechanisms discussed in this chapter, Smarty can cache the contents of the HTML page. Whether you use Smarty&#8217;s content cache or one of the alternatives discussed in this chapter, you can successfully use both template and content caching together on the same site.</p><p><strong>HTTP Headers and Output Buffering </strong></p><p>Output buffering can help solve the most common problem associated with the <code>header</code> function, not to mention the issues surrounding <code>session_start</code> and <code>set_cookie</code>. Normally, if you call any of these functions after page output has begun, you&#8217;ll get a nasty error message. When output buffering&#8217;s turned on, the only output types that can escape the buffer are HTTP headers. If you use ob_start at the very beginning of your application&#8217;s execution, you can send headers at whichever point you like, without encountering the usual errors. You can then write out the buffered page content all at once, when you&#8217;re sure that no more HTTP headers are required.</p><p><em>Use Output Buffering Responsibly<br
/> While output buffering can helpfully solve all our header problems, it should not be used solely for that reason. By ensuring that all output is generated after all the headers are sent, you&#8217;ll save the time and resource overheads involved in using output buffers.</em></p><h5>How do I cache just the parts of a page that change infrequently?</h5><p>Caching an entire page is a simplistic approach to output buffering. While it&#8217;s easy to implement, that approach negates the real benefits presented by PHP&#8217;s output control functions to improve your site&#8217;s performance in a manner that&#8217;s relevant to the varying lifetimes of your content.</p><p>No doubt, some parts of the page that you send to visitors will change very rarely, such as the page&#8217;s header, menus, and footer. But other parts&#8211;for example, the list of comments on your blog posts&#8211;may change quite often. Fortunately, PHP allows you to cache sections of the page separately.</p><p><strong><em>Solution</em></strong></p><p>Output buffering can be used to cache sections of a page in separate files. The page can then be rebuilt for output from these files.</p><p>This technique eliminates the need to repeat database queries, while loops, and so on. You might consider assigning each block of the page an expiry date after which the cache file is recreated; alternatively, you may build into your application a mechanism that deletes the cache file every time the content it stores is changed.</p><p>Let&#8217;s work through an example that demonstrates the principle. Firstly, we&#8217;ll create two helper functions, <code>writeCache</code> and <code>readCache</code>. Here&#8217;s the <code>writeCache</code> function:</p> <code>smartcache.php (excerpt)
&lt;?php
function writeCache($content, $filename)
{
$fp = fopen('./cache/' . $filename, 'w');
fwrite($fp, $content);
fclose($fp);
}</code><p>The <code>writeCache</code> function is quite simple; it just writes the content of the first argument to a file with the name specified in the second argument, and saves that file to a location in the cache directory. We&#8217;ll use this function to write our HTML to the cache files.</p><p>The <code>readCache</code> function will return the contents of the cache file specified in the first argument if it has not expired&#8211;that is, the file&#8217;s last modified time is not older than the current time minus the number of seconds specified in the second argument. If it has expired or the file does not exist, the function returns false:</p> <code>smartcache.php (excerpt)
function readCache($filename, $expiry)
{
if (file_exists('./cache/' . $filename))
{
if ((time() - $expiry) &gt; filemtime('./cache/' . $filename))
{
return false;
}
$cache = file('./cache/' . $filename);
return implode('', $cache);
}
return false;
}</code><p>For the purposes of demonstrating this concept, I&#8217;ve used a procedural approach. However, I wouldn&#8217;t recommend doing this in practice, as it will result in very messy code and is likely to cause issues with file locking. For example, what happens when someone accesses the cache at the exact moment it&#8217;s being updated? Better solutions will be explained later on in the chapter.</p><p>Let&#8217;s continue this example. After the output buffer is started, processing begins. First, the script calls <code>readCache</code> to see whether the file <code>header.cache</code> exists; this contains the top of the page&#8211;the HTML <code>&lt;head&gt;</code> tag and the start <code>&lt;body&gt;</code> tag. We&#8217;ve used PHP&#8217;s date function to display the time at which the page was actually rendered, so you&#8217;ll be able to see the different cache files at work when the page is displayed:</p> <code>smartcache.php (excerpt)
ob_start();
if (!$header = readCache('header.cache', 604800))
{
?&gt;
&lt;!DOCTYPE html public "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"&gt;
&lt;html xmlns="http://www.w3.org/1999/xhtml"&gt;
&lt;head&gt;
&lt;title&gt;Chunked Cached Page&lt;/title&gt;
&lt;meta http-equiv="Content-Type"
content="text/html; charset=iso-8859-1"/&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;p&gt;The header time is now: &lt;?php echo date('H:i:s'); ?&gt;&lt;/p&gt;
&lt;?php
$header = ob_get_contents();
ob_clean();
writeCache($header,'header.cache');
}</code><p>Note what happens when a cache file isn&#8217;t found: the header content is output and assigned to a variable, <code>$header</code>, with <code>ob_get_contents</code>, after which the <code>ob_clean</code> function is called to empty the buffer. This allows us to capture the output in &#8220;chunks&#8221; and assign them to individual cache files with the <code>writeCache</code> function. The header of the page is now stored as a file, which can be reused without our needing to rerender the page. Look back to the start of the if condition for a moment. When we called <code>readCache</code>, we gave it an expiry time of 604800 seconds (one week); <code>readCache</code> uses the file modification time of the cache file to determine whether the cache is still valid.</p><p>For the body of the page, we&#8217;ll use the same process as before. However, this time, when we call <code>readCache</code>, we&#8217;ll use an expiry time of five seconds; the cache file will be updated whenever it&#8217;s more than five seconds old:</p> <code>smartcache.php (excerpt)
if (!$body = readCache('body.cache', 5))
{
echo 'The body time is now: ' . date('H:i:s') . '&lt;br /&gt;';
$body = ob_get_contents();
ob_clean();
writeCache($body, 'body.cache');
}</code><p>The page footer is effectively the same as the header. After the footer, the output buffering is stopped and the contents of the three variables that hold the page data are displayed:</p> <code>smartcache.php (excerpt)
if (!$footer = readCache('footer.cache', 604800)) {
?&gt;
&lt;p&gt;The footer time is now: &lt;?php echo date('H:i:s'); ?&gt;&lt;/p&gt;
&lt;/body&gt;
&lt;/html&gt;
&lt;?php
$footer = ob_get_contents();
ob_clean();
writeCache($footer, 'footer.cache');
}
ob_end_clean();
echo $header . $body . $footer;
?&gt;</code><p>The end result looks like this:</p> <code>The header time is now: 17:10:42
The body time is now: 18:07:40
The footer time is now: 17:10:42</code><p>The header and footer are updated on a weekly basis, while the body is updated whenever it is more than five seconds old. If you keep refreshing the page, you&#8217;ll see the body time updating.</p><p><strong><em>Discussion</em></strong></p><p>Note that if you have a page that builds content dynamically, based on a number of variables, you&#8217;ll need to make adjustments to the way you handle your cache files. For example, you might have an online shopping catalog whose listing pages are defined by a URL such as:</p> <code>http://example.com/catalogue/view.php?category=1&amp;page=2</code><p>This URL should show page two of all items in category one; let&#8217;s say this is the category for socks. But if we were to use the caching code above, the results of the first page of the first category we looked at would be cached, and shown for any request for any other page or category, until the cache expiry time elapsed. This would certainly confuse the next visitor who wanted to browse the category for shoes&#8211;that person would see the cached content for socks!</p><p>To avoid this issue, you&#8217;ll need to incorporate the category ID and page number in to the cache file name like so:</p> <code> $cache_filename = 'catalogue_' . $category_id . '_' .
$page . '.cache';
if (!$catalogue = readCache($cache_filename, 604800))
{
...display the category HTML...
}</code><p>This way, the correct cached content can be retrieved for every request.</p><p><em>Nesting Buffers</em><br
/> <em>You can nest one buffer within another practically ad infinitum simply by calling ob_startmore than once. This can be useful if you have multiple operations that use the output buffer, such as one that catches the PHP error messages, and another that deals with caching. Care needs to be taken to make sure that <code>ob_end_flush</code> or <code>ob_end_clean</code> is called every time <code>ob_start</code> is used.</em></p><h5>How do I use <code>PEAR::Cache_Lite</code> for server-side caching?</h5><p>The previous solution explored the ideas behind output buffering using the PHP <code>ob_*</code> functions. Although we mentioned at the time, that approach probably isn&#8217;t the best way to meet to dual goals of keeping your code maintainable and having a reliable caching mechanism. It&#8217;s time to see how we can put a caching system into action in a manner that will be reliable and easy to maintain.</p><p><strong><em>Solution</em></strong></p><div
id="adz" class="vertical"><script type="text/javascript">/*<![CDATA[*/// < ![CDATA[
GA_googleFillSlot("Articles_6_300x250");
// ]]&gt;/*]]>*/</script></div><p>In the interests of keeping your code maintainable and having a reliable caching mechanism, it&#8217;s a good idea to delegate the responsibility of caching logic to classes you trust. In this case, we&#8217;ll use a little help from <code>PEAR::Cache_Lite</code> (version 1.7.2 is used in <a
class="sublink" href="http://pear.php.net/package/Cache_Lite/">the examples here</a>). <code>Cache_Lite</code> provides a solid yet easy-to-use library for caching, and handles issues such as: file locking; creating, checking for, and deleting cache files; controlling the output buffer; and directly caching the results from function and class method calls. More to the point, <code>Cache_Lite</code> should be relatively easy to apply to an existing application, requiring only minor code modifications.</p><p><code>Cache_Lite</code> has four main classes. First is the base class, <code>Cache_Lite</code>, which deals purely with creating and fetching cache files, but makes no use of output buffering. This class can be used alone for caching operations in which you have no need for output buffering, such as storing the contents of a template you&#8217;ve parsed with PHP.</p><p>The examples here will not use <code>Cache_Lite</code> directly, but will instead focus on the three subclasses. <code>Cache_Lite_Function</code> can be used to call a function or class method and cache the result, which might prove useful for storing a MySQL query result set, for example. The <code>Cache_Lite_Output</code> class uses PHP&#8217;s output control functions to catch the output generated by your script and store it in cache files; it allows you to perform tasks such as those we completed in &#8220;How do I cache just the parts of a page that change infrequently?&#8221;. The <code>Cache_Lite_File</code> class bases cache expiry on the timestamp of a master file, with any cache file being deemed to have expired if it is older than the timestamp.</p><p>Let&#8217;s work through an example that shows how you might use <code>Cache_Lite</code> to create a simple caching solution. When we&#8217;re instantiating any child classes of <code>Cache_Lite</code>, we must first provide an array of options that determine the behavior of <code>Cache_Lite</code> itself. We&#8217;ll look at these options in detail in a moment. Note that the <code>cacheDir</code> directory we specify must be one to which the script has read and write access:</p> <code>cachelite.php (excerpt)
&lt;?php
require_once 'Cache/Lite/Output.php';
$options = array(
'cacheDir' =&gt; './cache/',
'writeControl' =&gt; 'true',
'readControl' =&gt; 'true',
'fileNameProtection' =&gt; false,
'readControlType' =&gt; 'md5'
);
$cache = new Cache_Lite_Output($options);</code><p>For each chunk of content that we want to cache, we need to set a lifetime (in seconds) for which the cache should live before it&#8217;s refreshed. Next, we use the start method, available only in the <code>Cache_Lite_Output</code> class, to turn on output buffering. The two arguments passed to the start method are an identifying value for this particular cache file, and a cache group. The group is an identifier that allows a collection of cache files to be acted upon; it&#8217;s possible to delete all cache files in a given group, for example (more on this in a moment). The start method will check to see if a valid cache file is available and, if so, it will begin outputting the cache contents. If a cache file is not available, start will return false and begin caching the following output.</p><p>Once the output for this chunk has finished, we use the <code>end</code> method to stop buffering and store the content as a file:</p> <code>cachelite.php (excerpt)
$cache-&gt;setLifeTime(604800);
if (!$cache-&gt;start('header', 'Static')) {
?&gt;
&lt;!DOCTYPE html public "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"&gt;
&lt;html xmlns="http://www.w3.org/1999/xhtml"&gt;
&lt;head&gt;
&lt;title&gt;PEAR::Cache_Lite example&lt;/title&gt;
&lt;meta http-equiv="Content-Type"
content="text/html; charset=iso-8859-1"/&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;h2&gt;PEAR::Cache_Lite example&lt;/h2&gt;
&lt;p&gt;The header time is now: &lt;?php echo date('H:i:s'); ?&gt;&lt;/p&gt;
&lt;?php
$cache-&gt;end();
}</code><p>To cache the body and footer, we follow the same procedure we used for the header. Note that, again, we specify a five-second lifetime when caching the body:</p> <code>cachelite.php (excerpt)
$cache-&gt;setLifeTime(5);
if (!$cache-&gt;start('body', 'Dynamic')) {
echo 'The body time is now: ' . date('H:i:s') . '&lt;br /&gt;';
$cache-&gt;end();
}
$cache-&gt;setLifeTime(604800);
if (!$cache-&gt;start('footer', 'Static')) {
?&gt;
&lt;p&gt;The footer time is now: &lt;?php echo date('H:i:s'); ?&gt;&lt;/p&gt;
&lt;/body&gt;
&lt;/html&gt;
&lt;?php
$cache-&gt;end();
}
?&gt;</code><p>On viewing the page, <code>Cache_Lite</code> creates cache files in the cache directory. Because we&#8217;ve set the <code>fileNameProtection</code> option to false, <code>Cache_Lite</code> creates the files with these names:</p> <code>- ./cache/cache_Static_header
- ./cache/cache_Dynamic_body
- ./cache/cache_Static_footer</code><p>You can read about the <code>fileNameProtection</code> option&#8211;and many more&#8211;in &#8220;What configuration options does <code>Cache_Lite</code> support?&#8221;. When the same page is requested later, the code above will use the cached file if it is valid and has not expired.</p><p><em>Protect your Cache Files</em><br
/> <em>Make sure that the directory in which you place the cache files is not publicly available, or you may be offering your site&#8217;s visitors access to more than you realize.</em></p><h5>What configuration options does <code>Cache_Lite</code> support?</h5><p>When instantiating <code>Cache_Lite</code> (or any of its subclasses, such as <code>Cache_Lite_Output</code>), you can use any of a number of approaches to controlling its behavior. These options should be placed in an array and passed to the constructor as shown below (and in the previous section):</p> <code>$options = array(
'cacheDir' =&gt; './cache/',
'writeControl' =&gt; true,
'readControl' =&gt; true,
'fileNameProtection' =&gt; false,
'readControlType' =&gt; 'md5'
);
$cache = new Cache_Lite_Output($options);</code><p><strong><em>Solution</em></strong></p><p>The options available in the current version of <code>Cache_Lite</code> (1.7.2) are:</p><p><code>cacheDir</code><br
/> This is the directory in which the cache files will be placed. It defaults to <code>/tmp/</code>.</p><p><code>caching</code><br
/> This option switches on and off the caching behavior of <code>Cache_Lite</code>. If you have numerous <code>Cache_Lite</code> calls in your code and want to disable the cache for debugging, for example, this option will be important. The default value is <code>true</code> (caching enabled).</p><p><code>lifeTime</code><br
/> This option represents the default lifetime (in seconds) of cache files. It can be changed using the <code>setLifeTime</code> method. The default value is <code>3600</code> (one hour), and if it&#8217;s set to null, the cache files will never expire.</p><p><code>fileNameProtection</code><br
/> With this option activated, <code>Cache_Lite</code> uses an MD5 encryption hash to generate the filename for the cache file. This option protects you from error when you try to use IDs or group names containing characters that aren&#8217;t valid for filenames; <code>fileNameProtection</code> must be turned on when you use <code>Cache_Lite_Function</code>. The default is <code>true</code> (enabled).</p><p><code>fileLocking</code><br
/> This option is used to switch the file locking mechanisms on and off. The default is <code>true</code> (enabled).</p><p><code>writeControl</code><br
/> This option checks that a cache file has been written correctly immediately after it has been created, and throws a PEAR::Error if it finds a problem. Obviously, this facility would allow your code to attempt to rewrite a cache file that was created incorrectly, but it comes at a cost in terms of performance. The default value is <code>true</code> (enabled).</p><p><code>readControl</code><br
/> This option checks any cache files that are being read to ensure they&#8217;re not corrupt. Cache_Lite is able to place inside the file a value, such as the string length of the file, which can be used to confirm that the cache file isn&#8217;t corrupt. There are three alternative mechanisms for checking that a file is valid, and they&#8217;re specified using the <code>readControlType</code> option. These mechanisms come at the cost of performance, but should help to guarantee that your visitors aren&#8217;t seeing scrambled pages. The default value is <code>true</code> (enabled).</p><p><code>readControlType</code><br
/> This option lets you specify the type of read control mechanism you want to use. The available mechanisms are a cyclic redundancy check (<code>crc32</code>, the default value) using PHP&#8217;s <code>crc32</code> function, an MD5 hash using PHP&#8217;s <code>md5</code> function (<code>md5</code>), or a simple and fast string length check (<code>strlen</code>). Note that this mechanism is not intended to provide security from people tampering with your cache files; it&#8217;s just a way to spot corrupt files.</p><p><code>pearErrorMode</code><br
/> This option tells Cache_Lite how it should return PEAR errors to the calling script. The default is <code>CACHE_LITE_ERROR_RETURN</code>, which means Cache_Lite will return a PEAR::Error object.</p><p><code>memoryCaching</code><br
/> With memory caching enabled, every time a file is written to the cache, it is stored in an array in <code>Cache_Lite</code>. The <code>saveMemoryCachingState</code> and <code>getMemoryCachingState</code> methods can be used to store and access the memory cache data between requests. The advantage of this facility is that the complete set of cache files can be stored in a single file, reducing the number of disk read/write operations by reconstructing the cache files straight into an array to which your code has access. The <code>memoryCaching</code> option may be worth further investigation if you run a large site. The default value is <code>false</code> (disabled).</p><p><code>onlyMemoryCaching</code><br
/> If this option is enabled, only the memory caching mechanism will be used. The default value is <code>false</code> (disabled).</p><p><code>memoryCachingLimit</code><br
/> This option places a limit on the number of cache files that will be stored in the memory caching array. The more cache files you have, the more memory will be used up by memory caching, so it may be a good idea to enforce a limit that prevents your server from having to work too hard. Of course, this option places no restriction on the size of each cache file, so just one or two massive files may cause a problem. The default value is <code>1000</code>.</p><p><code>automaticSerialization</code><br
/> If enabled, this option will automatically serialize all data types. While this approach will slow down the caching system, it is useful for caching nonscalar data types such as objects and arrays. For higher performance, you might consider serializing nonscalar data types yourself. The default value is <code>false</code> (disabled).</p><p><code>automaticCleaningFactor</code><br
/> This option will automatically clean old cache entries&#8211;on average, one in x cache writes, where x is the value set for this option. Therefore, setting this value to <code>0</code> will indicate no automatic cleaning, and a value of 1will cause cache clearing on every cache write. A value of <code>20</code> to <code>200</code> is the recommended starting point if you wish to enable this facility; it causes cache cleaning to happen, on average, 0.5% to 5% of the time. The default value is <code>0</code> (disabled).</p><p><code>hashedDirectoryLevel</code><br
/> When set to a nonzero value, this option will enable a hashed directory structure. A hashed directory structure will improve the performance of sites that have thousands of cache files. If you choose to use hashed directories, start by setting this value to <code>1</code>, and increasing it as you test for performance improvements. The default value is <code>0</code> (disabled).</p><p><code>errorHandlingAPIBreak</code><br
/> This option was added to enable backwards compatibility with code that uses the old API. When the old API was run in <code>CACHE_LITE_ERROR_RETURN</code> mode (see the <code>pearErrorMode</code> option earlier in this list), some functions would return a Boolean value to indicate success, rather than returning a <code>PEAR_Error</code> object. By setting this value to true, the <code>PEAR_Error</code> object will be returned instead. The default value is <code>false</code> (disable).</p><h5>How do I purge the <code>Cache_Lite</code> cache?</h5><p>The built-in lifetime mechanism for <code>Cache_Lite</code> cache files provides a good foundation for keeping your cache files up to date, but there will be some circumstances in which you need the files to be updated immediately.</p><p><strong><em>Solution</em></strong></p><p>In cases in which you need immediate updates, the methods remove and clean come in handy. The remove method is designed to delete a specific cache file; it takes as arguments the cache ID and group name of the file. To delete the page body cache file we created in &#8220;How do I use PEAR::Cache_Lite for server-side caching?&#8221;, we&#8217;d use this code:</p> <code>$cache-&gt;remove('body', 'Dynamic');</code><p>If we use the clean method, we can delete all the files in our cache directory simply by calling the method with no arguments; alternatively, we can specify a group of cache files to delete. If we wanted to delete both the header and footer cache files we created in &#8220;How do I use PEAR::Cache_Lite for server-side caching?&#8221;, we could do so like this:</p> <code>$cache-&gt;clean('Static');</code><div
id="adz" class="vertical"><script type="text/javascript">/*<![CDATA[*/// < ![CDATA[
GA_googleFillSlot("Articles_6_300x250");
// ]]&gt;/*]]>*/</script></div><p><strong><em>Discussion</em></strong></p><p>The remove and clean methods should obviously be called in response to events that arise within an application. For example, if you have a discussion forum application, you probably want to remove the relevant cache files when a visitor posts a new message.</p><p>Although it may seem like this solution entails a lot of code modifications, with some care it can be applied to your application in a global manner. If you have a central script that&#8217;s included in every page, your script can simply watch for incoming events&#8211;for example, a variable like <code>$_GET['newPost']</code>&#8211;and respond by deleting the required cache files. This keeps the cache file removal mechanism central and easier to maintain. You might also consider using the <code>php.ini</code> setting <code>auto_prepend_file</code> to include this code in every PHP script.</p><h5>How do I cache function calls?</h5><p>Many web sites provide access to their data via web services such as SOAP and XML-RPC. (You can read all about web services in Chapter 12.) As web services are accessed over a network, it&#8217;s often a very good idea to cache results so that they can be fetched locally, rather than repeating the same slow request to the server multiple times. A simple approach might be to use PHP sessions, but as that solution operates on a per-visitor basis, the opening requests for each visitor will still be slow.</p><p><strong><em>Solution</em></strong></p><p>Let&#8217;s assume you wish to create a web page that lists all the SitePoint books available on Amazon. The actual list is not likely to change from moment to moment, so why would we make the request to the Amazon web service every time the web page is displayed? We won&#8217;t! Instead, we can take advantage of <code>Cache_Lite</code> by caching the results of the XML-RPC request.</p><p><em>Requires PEAR::SOAP Version 0.11.0 </em><br
/> <em>The following solution uses the PEAR::SOAP library version 0.11.0 to access the Amazon web service. You can find this package on the <a
class="sublink" href="http://pear.php.net/package/soap/">PEAR web site</a>.</em></p><p>Here&#8217;s some hypothetical code that fetches the data from the remote Amazon server:</p> <code>$results = $amazonClient-&gt;ManufacturerSearchRequest($params);</code><p>Using <code>Cache_Lite_Function</code>, we can cache the results so the data returned from the service can be reused; this will avoid unnecessary network calls and significantly improve performance.</p><p>The following example code focuses on the caching aspect to prevent us from getting bogged down in the details of using the Amazon web service. You can see the complete script if you download this book&#8217;s code archive from the SitePoint web site.</p><p>The <code>Cache_Lite_Function</code> requires the inclusion of the following file:</p> <code>cachefunction.php (excerpt)
require_once 'Cache/Lite/Function.php';</code><p>We instantiate the <code>Cache_Lite_Function</code> class with some options:</p> <code>cachefunction.php (excerpt)
$options = array(
'cacheDir' =&gt; './cache/',
'fileNameProtection' =&gt; true,
'writeControl' =&gt; true,
'readControl' =&gt; true,
'readControlType' =&gt; 'strlen',
'defaultGroup' =&gt; 'SOAP'
);
$cache = new Cache_Lite_Function($options);</code><p>It&#8217;s important that the <code>fileNameProtection</code> option is set to <code>true </code>(this is in fact the default value, but in this case I&#8217;ve set it manually to emphasize the point). If it were set to <code>false</code>, the filename would be invalid, so the data will not be cached.</p><p>Here&#8217;s how we make the calls to our SOAP client class:</p> <code>cachefunction.php (excerpt)
$results = $cache-&gt;call('amazonClient-&gt;ManufacturerSearchRequest',
$params);</code><p>If the request is being made for the first time, <code>Cache_Lite_Function</code> will store the results as a serialized array or object in a cache file (not that you need to worry about this), and this file will be used for future requests until it expires. The <code>setLifeTime</code> method can again be used to specify how long the cache files should survive before they&#8217;re refreshed; currently, the default value of 3600 seconds (one hour) is being used. You can then use the <code>$results</code> variable exactly as if you were calling the web service method directly. The output of our example script can be seen in Figure 11.1.</p><p><img
src="http://i2.sitepoint.com/graphics/amazonbooks.thumb.png" alt="SitePoint books at Amazon" width="400" height="344" /></p><h5>Summary</h5><p>Caching is an important and often overlooked aspect of web site development. Many factors that affect the performance of today&#8217;s web sites weren&#8217;t a problem for their predecessors&#8211;from complex, dynamic page generation, to a reliance on third-party data over the network. In this chapter, we&#8217;ve examined HTML meta tags, HTTP headers, PHP output buffering and <code>PEAR::Cache_Lite</code>, and we&#8217;ve seen how you can use them to control the caching of your web site content and improve the site&#8217;s reliability and performance.</p><p>Implementing a caching system for your site might be simple, but ultimately, it depends on your requirements. If you have a busy and predominantly static web site&#8211;such as a blog&#8211;that&#8217;s managed through a content management system, it will likely require little alteration, yet may benefit from huge performance improvements resulting from a small investment of your time. Setting up caching for a more complex site that generates content on a per-user basis, such as a portal or shopping cart system, will prove a little more tricky and time consuming, but the benefits are still clear.</p><p>Regardless, I hope the information in this chapter has given you a good grasp of the options available, and will help you determine which techniques are most suitable for your application. Don&#8217;t forget to <a
class="sublink" href="http://www.sitepoint.com/launch/108ef2/2/120">download this chapter, plus two others</a> &#8212; PDO and Databases, and Access Control &#8212; to enjoy offline. For information on the contents of the book&#8217;s other chapters, check out the <a
class="sublink" href="http://www.sitepoint.com/books/phpant2/toc.php">full Table of Contents</a>.</p><div
id="adz" class="ad size300x250_2"><script type="text/javascript">GA_googleFillSlot("Blogs_300x250_2");</script></div><div
id="spacer" style="display: block; clear:both"></div><h2 class="older-posts"> <span
class="alignleft"><a
href="http://blogs.sitepoint.com/the-php-anthology-101-essential-tips-tricks-hacks-2nd-edition/" rel="prev">&laquo; Previous in Category</a></span> <span
class="alignright"><a
href="http://blogs.sitepoint.com/index-of-php-tokens-for-emacs-and-beyond/" rel="next">Next in Category &raquo;</a></span></h2><div
id="post-footer"><p
id="post-date"> <em> Posted on <strong
class="date">November 7, 2007</strong> in <a
href="/blogs/category/php" title="More in &#8220;PHP&#8221;">PHP</a> and tagged as: .
The views and opinions in this blog post are those of its author. </em></p></div></div><div
style="clear:both"><div
class="ad"><script type="text/javascript">GA_googleFillSlot("Blogs_13a_300x100");</script></div><div
class="ad"><script type="text/javascript">GA_googleFillSlot("Blogs_13b_300x100");</script></div></div></div><div
id="secondary-content" class="column"><div
id="social-networking"><h2 class="structural-label">Social Networking</h2><p
class="sidebox twitter"> <a
href="http://www.twitter.com/sitepointdotcom"> <img
src="http://blogs.sitepoint.netdna-cdn.com/wp-content/themes/sitepoint_2010/images/twitter32x32.png" alt="" /> <strong>Follow us on Twitter.</strong> 70,892 followers and counting! </a></p><p
class="sidebox facebook"> <a
href="http://www.facebook.com/sitepoint"> <img
src="http://blogs.sitepoint.netdna-cdn.com/wp-content/themes/sitepoint_2010/images/facebook32x32.png" alt="" /> <strong>Become a fan on Facebook.</strong> We love getting to know you. </a></p> <span
class="divider"></span></div><div
id="book-promo"> <a
href="http://www.sitepoint.com/launch/a5e94e"> <img
src="http://www.sitepoint.com/images/books/wdbk3/ads/250x250-fail.jpg" alt="The Web Design Business Kit" width="250" height="250" /> </a></div><div
id="recent-comments" class="sidebox"><h2>Recent Comments</h2><ul
class="recent-comments"><li><cite>Andrew<img
style='float: left; margin-right: 0px; border: none; margin-right:5px' src='http://www.gravatar.com/avatar/c11c35b59e444d3b33b8f0701072f2a3?rating=R&amp;default=monsterid' alt='No Avatar' width=40 height=40/></cite> in <a
class="title"  href="http://blogs.sitepoint.com/animating-with-jquery/#comment-964164">Animating with jQuery</a></li><li><cite>Adam<img
style='float: left; margin-right: 0px; border: none; margin-right:5px' src='http://www.gravatar.com/avatar/92cf58b81a49926f4754c69171dedda4?rating=R&amp;default=monsterid' alt='No Avatar' width=40 height=40/></cite> in <a
class="title"  href="http://blogs.sitepoint.com/animating-with-jquery/#comment-964162">Animating with jQuery</a></li><li><cite>Pam - Ryvon<img
style='float: left; margin-right: 0px; border: none; margin-right:5px' src='http://www.gravatar.com/avatar/8330a7c4a8fb5c05668d84d43c65a8ad?rating=R&amp;default=monsterid' alt='No Avatar' width=40 height=40/></cite> in <a
class="title"  href="http://blogs.sitepoint.com/five-freelancing-tips-to-avoid-disasters/#comment-964161">Five Freelancing Tips to Avoid Disasters</a></li><li><cite>http://www.makeyourownwebsiteeasy.com/<img
style='float: left; margin-right: 0px; border: none; margin-right:5px' src='http://www.gravatar.com/avatar/09a6335da6ae748bb0568235ecabb229?rating=R&amp;default=monsterid' alt='No Avatar' width=40 height=40/></cite> in <a
class="title"  href="http://blogs.sitepoint.com/five-freelancing-tips-to-avoid-disasters/#comment-964160">Five Freelancing Tips to Avoid Disasters</a></li><li><cite>ServerStorm<img
style='float: left; margin-right: 0px; border: none; margin-right:5px' src='http://www.gravatar.com/avatar/6611d2f45d9582cf2090a7224259b94f?rating=R&amp;default=monsterid' alt='No Avatar' width=40 height=40/></cite> in <a
class="title"  href="http://blogs.sitepoint.com/give-floats-the-flick-in-css-layouts/#comment-964158">Give Floats the Flick in CSS Layouts</a></li></ul></div></div> <script type="text/javascript">GA_googleFillSlot("Blogs_160x600_SkyScraper");</script> </div></div></div><div
id="footer"><hr
/><p
id="copyright"> The contents of this webpage are copyright &#169; 1998-2011 SitePoint Pty. Ltd. <a
href="http://www.sitepoint.com/about/terms">All Rights Reserved.</a></p><h2 class="structural-label">Footer links</h2><ul
id="footer-links"><li
id="learn"><h3>Learn</h3><ul><li><a
href="http://articles.sitepoint.com/">Find a Tutorial</a></li><li><a
href="http://www.sitepoint.com/blogs">Read Latest News</a></li><li><a
href="http://reference.sitepoint.com">Browse the Reference</a></li><li><a
href="http://www.sitepoint.com/newsletters">Subscribe to a Newsletter</a></li><li><a
href="http://www.sitepoint.com/books">Buy a Book</a></li><li><a
href="http://www.sitepoint.com/videos">Watch a Video</a></li><li><a
href="http://tools.sitepoint.com">Download Free Tools</a></li></ul></li><li
id="get-involved"><h3>Get Involved!</h3><ul><li><a
href="http://www.sitepoint.com/about/writeforus">Write For Us</a></li><li><a
href="http://www.sitepoint.com/forums/register.php">Join The Forums</a></li><li><a
href="http://www.sitepoint.com/podcast">Listen to the Podcast</a></li><li><a
href="http://www.sitepoint.com/subscribe">Subscribe via RSS</a></li><li><a
href="http://marketplace.sitepoint.com">Post a Classified Ad</a></li></ul></li><li
id="about"><h3>About</h3><ul
class="about"><li><a
href="http://www.sitepoint.com/about">The SitePoint Story</a></li><li><a
href="http://www.sitepoint.com/about/jobs">Jobs at SitePoint</a></li></ul></li><li
id="contact"><h3>Contact</h3><ul><li><a
href="http://www.sitepoint.com/contact">Customer Support</a></li><li><a
href="http://www.sitepoint.com/mediakit">Advertise With Us</a></li></ul></li><li
id="help"><h3>Help</h3><ul><li><a
href="http://search.sitepoint.com">Search</a></li><li><a
href="http://www.sitepoint.com/sitemap" accesskey="3">Site Map</a></li></ul></li><li
id="social"><ul><li> <a
href="http://www.twitter.com/sitepointdotcom"> <img
alt="" src="http://blogs.sitepoint.netdna-cdn.com/wp-content/themes/sitepoint_2010/images/twitter22x22.png" /> <strong>Follow Us</strong></a></li><li> <a
href="http://www.facebook.com/sitepoint"> <img
alt="" src="http://blogs.sitepoint.netdna-cdn.com/wp-content/themes/sitepoint_2010/images/facebook22x22.png" /> <strong>Become a fan</strong></a></li></ul></li></ul><div
id="meta-info"><ul><li
class="solutions"> <a
href="http://www.sitepoint.com.au" class="solutions">Web Design &amp; Development by SitePoint Solutions, Melbourne, Australia</a> | <a
href="http://www.99designs.com" class="design">99designs.com</a></li><li
class="top"> <a
href="#sitepoint-logo">Back to top</a></li></ul></div></div>  <script type="text/javascript">var gaJsHost = (("https:" == document.location.protocol) ? "https://ssl." : "http://www.");
	document.write(unescape("%3Cscript src='" + gaJsHost + "google-analytics.com/ga.js' type='text/javascript'%3E%3C/script%3E"));</script> <script type="text/javascript">try{
		var sitepointTracker = _gat._getTracker("UA-30131-1");
		sitepointTracker._setDomainName(".sitepoint.com");
		sitepointTracker._setAllowHash(false);
		sitepointTracker._setCustomVar(1, "Context", "Blogs", 3);
		sitepointTracker._trackPageview();
	} catch(err) {}</script> <div
id="ClickTaleDiv" style="display: none;"></div> <script type='text/javascript'>document.write(unescape("%3Cscript%20src='"+
 (document.location.protocol=='https:'?
  'https://clicktale.pantherssl.com/':
  'http://s.clicktale.net/')+
 "WRb.js'%20type='text/javascript'%3E%3C/script%3E"));</script> <script type="text/javascript">var ClickTaleSSL=1;
if(typeof ClickTale=='function') ClickTale(39240,0.005,"www");</script> <div> <img
alt="" src="http://ad.adtegrity.net/pixel?id=613372&amp;t=2" width="1" height="1" /></div> <script type='text/javascript' src='http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js?ver=1.4.2'></script> <script src="http://stats.wordpress.com/e-201112.js" type="text/javascript"></script> <script type="text/javascript">st_go({blog:'15923422',v:'ext',post:'34710'});
var load_cmc = function(){linktracker_init(15923422,34710,2);};
if ( typeof addLoadEvent != 'undefined' ) addLoadEvent(load_cmc);
else load_cmc();</script> </body></html>
<!-- Performance optimized by W3 Total Cache. Learn more: http://www.w3-edge.com/wordpress-plugins/

Page Caching using memcached
Database Caching 6/21 queries in 0.165 seconds using memcached
Object Caching 1203/1227 objects using memcached
Content Delivery Network via blogs.sitepoint.netdna-cdn.com

Served from: blogs.sitepoint.com @ 2011-03-25 09:06:01 -->